/*! ******************************************************************************
 *
 * Pentaho
 *
 * Copyright (C) 2024 by Hitachi Vantara, LLC : http://www.pentaho.com
 *
 * Use of this software is governed by the Business Source License included
 * in the LICENSE.TXT file.
 *
 * Change Date: 2029-07-20
 ******************************************************************************/


package org.pentaho.platform.plugin.action.chartbeans;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.pentaho.actionsequence.dom.ActionSequenceDocument;
import org.pentaho.actionsequence.dom.IActionSequenceDocument;
import org.pentaho.actionsequence.dom.IActionSequenceInput;
import org.pentaho.actionsequence.dom.IActionSequenceInputSource;
import org.pentaho.actionsequence.dom.IActionSequenceOutput;
import org.pentaho.actionsequence.dom.actions.ActionDefinition;
import org.pentaho.actionsequence.dom.actions.MQLAction;
import org.pentaho.chart.model.ChartDataDefinition;
import org.pentaho.chart.model.ChartModel;
import org.pentaho.chart.model.util.ChartSerializer;
import org.pentaho.chart.model.util.ChartSerializer.ChartSerializationFormat;
import org.pentaho.chart.plugin.jfreechart.JFreeChartPlugin;
import org.pentaho.chart.plugin.openflashchart.OpenFlashChartPlugin;
import org.pentaho.platform.api.engine.ILogger;
import org.pentaho.platform.api.engine.IOutputHandler;
import org.pentaho.platform.api.engine.IParameterProvider;
import org.pentaho.platform.api.engine.IPentahoRequestContext;
import org.pentaho.platform.api.engine.IPentahoSession;
import org.pentaho.platform.api.engine.IPentahoUrlFactory;
import org.pentaho.platform.api.engine.IRuntimeContext;
import org.pentaho.platform.api.engine.ISolutionEngine;
import org.pentaho.platform.config.messages.Messages;
import org.pentaho.platform.engine.core.output.SimpleOutputHandler;
import org.pentaho.platform.engine.core.solution.SimpleParameterProvider;
import org.pentaho.platform.engine.core.system.PentahoRequestContextHolder;
import org.pentaho.platform.engine.core.system.PentahoSystem;
import org.pentaho.platform.engine.services.runtime.TemplateUtil;
import org.pentaho.platform.plugin.action.pentahometadata.ActionDefinitionEncoder;
import org.pentaho.platform.util.UUIDUtil;
import org.pentaho.platform.util.web.SimpleUrlFactory;

import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.Set;

public class DefaultChartBeansGenerator implements IChartBeansGenerator {

  private Random random = new Random();
  private static final String URL_PATH_SEP = "/";
  private static final String DEFAULT_HTML_TEMPLATE =
      "<html><head><title>Command: doChart</title>{0}</head><body style='margin:0;' scroll='no'>{1}</body></html>";
  //$NON-NLS-1$

  private static final String JFREE_CHART_HTML_TEMPLATE =
      "<html><head><title>Pentaho Chart</title></head><body style='margin:0;' scroll='no'>{imageMap}<img border=\"0\" "
          + "width=\"{chartWidth}\" height=\"{chartHeight}\" src=\"{contextPath}getImage?image={imageFile}\" "
          + "{useMap}/></body></html>";
  //$NON-NLS-1$

  private static String DEFAULT_flashScriptFragment =
      "<input type=\"hidden\"/><script defer='defer' type=\"text/javascript\">function {dataFunction}() { return "
          + "/*JSON*/\"{chartJson}\"/*END_JSON*/;}</script>";
  //$NON-NLS-1$

  private static String DEFAULT_flashObjectFragment = "<object classid=\"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000\" " //$NON-NLS-1$
      + "codebase=\"http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=8,0,0,0\" "
      //$NON-NLS-1$
      + "width=\"{chart-width}\" height=\"{chart-height}\"  id=\"ofco{chartId}\" align=\"middle\"> " //$NON-NLS-1$
      + "<param name=\"allowScriptAccess\" value=\"sameDomain\" /> " //$NON-NLS-1$
      + "<param name=\"wmode\" value=\"opaque\">" //$NON-NLS-1$
      + "<param name=\"movie\" value=\"{ofc-url}?get-data={dataFunction}\" /> " //$NON-NLS-1$
      + "<param name=\"quality\" value=\"high\" /> " //$NON-NLS-1$
      + "<embed src=\"{ofc-url}?get-data={dataFunction}\" wmode=\"opaque\" quality=\"high\" bgcolor=\"#FFFFFF\" "
      //$NON-NLS-1$
      + "width=\"{chart-width}\" height=\"{chart-height}\"  id=\"ofce{chartId}\" align=\"middle\" "
      + "allowScriptAccess=\"sameDomain\" type=\"application/x-shockwave-flash\" "
      //$NON-NLS-1$
      + "pluginspage=\"http://www.macromedia.com/go/getflashplayer\" /></object>"; //$NON-NLS-1$

  public DefaultChartBeansGenerator() {

  }

  /**
   * The engine that processes the parameters from the specific interface methods and writes a chart to the output
   * stream or returns an input stream for reading.
   * 
   * @param pentahoSession
   * @param serializedChartDataDefinition
   * @param serializedChartModel
   * @param chartWidth
   * @param chartHeight
   * @param outputStream
   * @return
   * @throws IOException
   */

  private InputStream internalCreateChart( IPentahoSession pentahoSession, Map<String, Object> parameterMap,
      String serializedChartDataDefinition, String serializedChartModel, int chartWidth, int chartHeight,
      String contentLinkingTemplate, OutputStream outputStream ) throws IOException {
    InputStream result = null;
    ByteArrayOutputStream resultOutputStream = null;
    OutputStream out = null;

    // Make code more readable by defining the output result
    boolean returnInputStream = outputStream == null ? true : false;

    // If the caller sends a null OutputStream, then we will return an InputStream
    if ( returnInputStream ) {
      resultOutputStream = new ByteArrayOutputStream();
      out = new BufferedOutputStream( resultOutputStream );
    } else {
      out = outputStream;
    }

    // Setup parameters to be passed to the xaction
    Map<String, Object> params = new HashMap<String, Object>();

    params.put( "chart-model-json", serializedChartModel ); //$NON-NLS-1$
    params.put( "chart-width", chartWidth ); //$NON-NLS-1$
    params.put( "chart-height", chartHeight ); //$NON-NLS-1$

    // De-serialize the chartDataDefintion and extract relevant parts
    ChartDataDefinition chartDataDefinition =
        ChartSerializer.deSerializeDataDefinition( serializedChartDataDefinition, ChartSerializationFormat.JSON );

    if ( chartDataDefinition.getQuery() != null ) {
      params.put( "query", chartDataDefinition.getQuery() ); //$NON-NLS-1$
    }

    if ( chartDataDefinition.getDomainColumn() != null ) {
      params.put( "series-column", chartDataDefinition.getDomainColumn() ); //$NON-NLS-1$
    }

    if ( chartDataDefinition.getCategoryColumn() != null
      && !chartDataDefinition.getCategoryColumn().equals( "None" ) ) {
      params.put( "category-column", chartDataDefinition.getCategoryColumn() ); //$NON-NLS-1$
    }

    if ( chartDataDefinition.getRangeColumn() != null ) {
      params.put( "value-column", chartDataDefinition.getRangeColumn() ); //$NON-NLS-1$
    }

    if ( chartDataDefinition.getScalingFactor() != null ) {
      params.put( "scaling-factor", chartDataDefinition.getScalingFactor().toString() ); //$NON-NLS-1$
    }

    createAndRunActionSequence( pentahoSession, params, parameterMap, contentLinkingTemplate, out );

    if ( out instanceof BufferedOutputStream ) {
      out.flush();
    }

    if ( returnInputStream ) {
      result = new ByteArrayInputStream( resultOutputStream.toByteArray() );

      return ( result );
    }

    return null;
  }

  protected void createAndRunActionSequence( final IPentahoSession pentahoSession, final Map<String, Object> params,
      final Map<String, Object> defaultParameterMap, String contentLinkingTemplate, final OutputStream out ) {

    SimpleParameterProvider parameterProvider = new SimpleParameterProvider( params );

    // add the default parameter values
    for ( Map.Entry<String, Object> entry : defaultParameterMap.entrySet() ) {
      parameterProvider.setParameter( entry.getKey(), entry.getValue() );
    }

    Map<String, IParameterProvider> parameterProviders = new HashMap<String, IParameterProvider>();
    parameterProviders.put( IParameterProvider.SCOPE_REQUEST, parameterProvider );

    SimpleOutputHandler outputHandler = new SimpleOutputHandler( out, true );
    outputHandler.setOutputPreference( IOutputHandler.OUTPUT_TYPE_DEFAULT );

    ActionSequenceDocument doc = createActionSequenceDocument( defaultParameterMap.keySet(), contentLinkingTemplate );
    runActionSequence( pentahoSession, parameterProviders, outputHandler, doc );

    try {
      out.flush();
      out.close();
    } catch ( IOException e ) {
      e.printStackTrace();
    }
  }

  /**
   * Executes an action sequence from an <code>ActionSequenceDocument</code>.
   * 
   * @param pentahoSession
   *          current <code>IPentahoSession</code>
   * @param parameterProviders
   *          map of parameter providers; there should a single entry with "request" as the key
   * @param outputHandler
   *          output handler
   * @param doc
   *          action sequence document
   * @throws RuntimeException
   *           if anything goes wrong
   */
  protected void runActionSequence( final IPentahoSession pentahoSession,
      final Map<String, IParameterProvider> parameterProviders, final IOutputHandler outputHandler,
      final ActionSequenceDocument doc ) throws RuntimeException {

    // Get the solution engine
    ISolutionEngine solutionEngine = PentahoSystem.get( ISolutionEngine.class, pentahoSession );
    if ( solutionEngine == null ) {
      throw new RuntimeException( "solutionEngine is null" ); //$NON-NLS-1$
    }
    solutionEngine.setLoggingLevel( ILogger.DEBUG );
    solutionEngine.init( pentahoSession );

    IPentahoRequestContext requestContext = PentahoRequestContextHolder.getRequestContext();
    String contextPath = requestContext.getContextPath();

    IPentahoUrlFactory urlFactory = new SimpleUrlFactory( contextPath ); //$NON-NLS-1$

    IRuntimeContext runtime;
    IParameterProvider requestParmProvider = parameterProviders.get( "request" );
    if ( requestParmProvider.hasParameter( "obj_id" ) ) {

      final String obj_id = (String) requestParmProvider.getParameter( "obj_id" );
      final String msg_name = (String) requestParmProvider.getParameter( "message_name" );
      final String job_id = (String) requestParmProvider.getParameter( "job_id" );

      runtime = solutionEngine.execute( doc.toString(), obj_id, job_id, false, true, //$NON-NLS-1$ //$NON-NLS-2$
          msg_name, true, parameterProviders, outputHandler, null, urlFactory, new ArrayList() ); //$NON-NLS-1$
    } else {
      runtime = solutionEngine.execute( doc.toString(), "chartbeans_mql", "myprocessid", false, true, //$NON-NLS-1$ //$NON-NLS-2$
          "myinstanceid", true, parameterProviders, outputHandler, null, urlFactory, new ArrayList() ); //$NON-NLS-1$
    }

    if ( ( runtime != null ) && ( runtime.getStatus() != IRuntimeContext.RUNTIME_STATUS_SUCCESS ) ) {
      StringBuilder buf = new StringBuilder();
      boolean firstIteration = true;
      for ( Object /* String */message : runtime.getMessages() ) {

        if ( message instanceof Exception ) {
          Exception ex = (Exception) message;
          if ( ex.getCause() instanceof RuntimeException ) {
            throw (RuntimeException) ex.getCause();
          }
        }

        if ( !firstIteration ) {
          buf.append( " \\\\ " ); //$NON-NLS-1$
        }
        buf.append( message );
      }
      String errorStr;
      if ( buf.indexOf( "action_sequence_failed" ) > -1 && buf.indexOf( "MQLRelationalDataComponent" ) > -1 ) {
        errorStr = Messages.getInstance().getString( "DefaultChartBeansGenerator.ERROR_0001_SECURITY_ERROR" );
      } else {
        errorStr = Messages.getInstance().getString( "DefaultChartBeansGenerator.ERROR_0002_UNKNOWN_ERROR" );
      }

      throw new RuntimeException( errorStr );
    }

  }

  /**
   * Creates an <code>ActionSequenceDocument</code> that will run an MQL query and pipe the results in the ChartBeans
   * <code>ChartComponent</code>.
   * 
   * @param parameterNameSet
   *          set of parameter names that appear in the MQL query
   * @return doc
   */
  protected ActionSequenceDocument createActionSequenceDocument( final Set<String> parameterNameSet,
      String contentLinkingTemplate ) {
    ActionSequenceDocument actionSequenceDocument = new ActionSequenceDocument();
    actionSequenceDocument.setTitle( "chartbeans_mql.xaction" ); //$NON-NLS-1$
    actionSequenceDocument.setVersion( "1" ); //$NON-NLS-1$
    actionSequenceDocument.setLoggingLevel( "debug" ); //$NON-NLS-1$
    actionSequenceDocument.setAuthor( "Dashboard" ); //$NON-NLS-1$
    actionSequenceDocument.setDescription( "Generate a chart through ChartBeans from an MQL statement." ); //$NON-NLS-1$
    actionSequenceDocument.setHelp( "Pass in an MQL statement that returns a table of three columns. The first column " //$NON-NLS-1$
        + "is the series, the second is the category and the third is the data." ); //$NON-NLS-1$
    actionSequenceDocument.setHelp( "" ); //$NON-NLS-1$
    actionSequenceDocument.setResultType( "rule" ); //$NON-NLS-1$

    IActionSequenceInput queryInput = actionSequenceDocument.createInput( "query", IActionSequenceDocument.STRING_TYPE ); //$NON-NLS-1$
    IActionSequenceInput chartModelJsonInput = actionSequenceDocument.createInput( "chart-model-json", IActionSequenceDocument.STRING_TYPE ); //$NON-NLS-1$
    IActionSequenceInput chartWidthInput = actionSequenceDocument.createInput( "chart-width", IActionSequenceDocument.INTEGER_TYPE ); //$NON-NLS-1$
    chartWidthInput.addSource( IActionSequenceDocument.REQUEST_INPUT_SOURCE, "chart-width" ); //$NON-NLS-1$
    chartWidthInput.setDefaultValue( "1" ); //$NON-NLS-1$
    IActionSequenceInput chartHeightInput = actionSequenceDocument.createInput( "chart-height", IActionSequenceDocument.INTEGER_TYPE ); //$NON-NLS-1$
    chartHeightInput.addSource( IActionSequenceDocument.REQUEST_INPUT_SOURCE, "chart-height" ); //$NON-NLS-1$
    chartHeightInput.setDefaultValue( "1" ); //$NON-NLS-1$
    IActionSequenceInput seriesColumnInput = actionSequenceDocument.createInput( "series-column", IActionSequenceDocument.STRING_TYPE ); //$NON-NLS-1$
    seriesColumnInput.setDefaultValue( "1" ); //$NON-NLS-1$
    IActionSequenceInput categoryColumnInput = actionSequenceDocument.createInput( "category-column", IActionSequenceDocument.STRING_TYPE ); //$NON-NLS-1$
    // set a default value of empty string to avoid an error when rendering pie charts (which don't have a category
    // column
    categoryColumnInput.setDefaultValue( "2" ); //$NON-NLS-1$
    IActionSequenceInput valueColumnInput = actionSequenceDocument.createInput( "value-column", IActionSequenceDocument.STRING_TYPE ); //$NON-NLS-1$
    valueColumnInput.setDefaultValue( "0" ); //$NON-NLS-1$
    IActionSequenceInput scalingFactorInput = actionSequenceDocument.createInput( "scaling-factor", IActionSequenceDocument.STRING_TYPE ); //$NON-NLS-1$

    // add inputs from parameterNameSet; these parameters will appear as placeholders in the query input
    for ( String parameterName : parameterNameSet ) {
      IActionSequenceInput input =
          actionSequenceDocument.createInput( ActionDefinitionEncoder.encodeBlankSpaces( parameterName ), IActionSequenceDocument.STRING_TYPE );
      IActionSequenceInputSource[] sources = input.getSources();
      if ( sources.length > 0 ) {
        input.getSources()[0].setName( parameterName );
      } else {
        input.addSource( "request", parameterName );
      }
    }

    IActionSequenceOutput outputStreamOutput = actionSequenceDocument.createOutput( "outputstream", IActionSequenceDocument.CONTENT_TYPE ); //$NON-NLS-1$
    outputStreamOutput.addDestination( IActionSequenceDocument.RESPONSE_OUTPUT_DESTINATION, "content" ); //$NON-NLS-1$

    MQLAction mqlAction = (MQLAction) actionSequenceDocument.addAction( MQLAction.class );
    mqlAction.setActionInputValue( "query", queryInput ); //$NON-NLS-1$

    // add inputs from parameterNameSet to this action
    for ( String parameterName : parameterNameSet ) {
      mqlAction.addInput( ActionDefinitionEncoder.encodeBlankSpaces( parameterName ), IActionSequenceDocument.STRING_TYPE );
    }

    mqlAction.setOutputResultSet( "chartdata" ); //$NON-NLS-1$
    mqlAction.setComponentDefinition( "live", Boolean.TRUE.toString() ); //$NON-NLS-1$
    mqlAction.setComponentDefinition( "display-names", Boolean.FALSE.toString() ); //$NON-NLS-1$

    ActionDefinition pojoAction = (ActionDefinition) actionSequenceDocument.addAction( ActionDefinition.class );
    pojoAction.setComponentName( "ChartBeansComponent" ); //$NON-NLS-1$
    if ( contentLinkingTemplate != null ) {
      pojoAction.setComponentDefinition( "contentLinkingTemplate", contentLinkingTemplate );
    }
    pojoAction.setActionInputValue( "chart-model-json", chartModelJsonInput ); //$NON-NLS-1$
    pojoAction.addInput( "chartdata", IActionSequenceDocument.RESULTSET_TYPE ); //$NON-NLS-1$
    pojoAction.setActionInputValue( "chart-width", chartWidthInput ); //$NON-NLS-1$
    pojoAction.setActionInputValue( "chart-height", chartHeightInput ); //$NON-NLS-1$
    pojoAction.setActionInputValue( "series-column", seriesColumnInput ); //$NON-NLS-1$
    pojoAction.setActionInputValue( "category-column", categoryColumnInput ); //$NON-NLS-1$
    pojoAction.setActionInputValue( "value-column", valueColumnInput ); //$NON-NLS-1$
    pojoAction.setActionInputValue( "scaling-factor", scalingFactorInput ); //$NON-NLS-1$
    pojoAction.addOutput( "outputstream", IActionSequenceDocument.CONTENT_TYPE ); //$NON-NLS-1$

    return actionSequenceDocument;
  }

  public String buildEmptyOpenFlashChartHtmlFragment( String msg ) {
    // populate the flash html template
    Properties props = new Properties();
    props.setProperty( "dataFunction", "getChartData" + Integer.toString( random.nextInt( 100000 ) ) ); // + chartId); //$NON-NLS-1$ //$NON-NLS-2$
    props.setProperty( "chartJson", "{}" ); //$NON-NLS-1$ //$NON-NLS-2$

    String flashHtml =
        MessageFormat.format( getHtmlTemplate(), new String[] {
          TemplateUtil.applyTemplate( getFlashScriptFragment(), props, null ), msg } );

    return flashHtml;
  }

  /**
   * Does this method belong in ChartBeansGeneratorUtil? ChartBeansGeneratorUtil may be more of a convenience for
   * executing the default ActionSequence, if this is to hold true, this method probably needs a new home more central
   * to the ChartBeans code. Returns a complete HTML document that references an Open Flash Chart SWF resource that
   * resides on the server along with the data that should be displayed in the chart (via a JavaScript function that
   * returns a JSON string).
   * <p>
   * Only exposed for debugging (i.e. hosted mode) purposes.
   * </p>
   */
  public String buildOpenFlashChartHtmlFragment( String openFlashChartJson, String swfUrl, String chartWidth,
      String chartHeight ) {
    // generate a unique name for the function
    String chartId = UUIDUtil.getUUIDAsString().replaceAll( "[^\\w]", "" ); //$NON-NLS-1$ //$NON-NLS-2$

    // populate the flash html template
    Properties props = new Properties();
    props.setProperty( "chartId", chartId ); //$NON-NLS-1$
    props.setProperty( "dataFunction", "getChartData" + Integer.toString( random.nextInt( 100000 ) ) ); // + chartId); //$NON-NLS-1$ //$NON-NLS-2$
    props.setProperty( "chart-width", chartWidth ); //$NON-NLS-1$
    props.setProperty( "chart-height", chartHeight ); //$NON-NLS-1$
    props.setProperty( "ofc-url", swfUrl ); //$NON-NLS-1$
    props.setProperty( "chartJson", openFlashChartJson ); //$NON-NLS-1$

    String flashHtml =
        MessageFormat.format( getHtmlTemplate(), new String[] {
          TemplateUtil.applyTemplate( getFlashScriptFragment(), props, null ),
          TemplateUtil.applyTemplate( getFlashObjectFragment(), props, null ) } );

    return flashHtml;
  }

  public String buildJFreeChartHtmlFragment( File imageFile, String imageMap, String imageMapName, int chartWidth,
      int chartHeight, String contextPath ) {

    Properties props = new Properties();
    props.setProperty( "imageMap", imageMap != null ? imageMap : "" ); //$NON-NLS-1$ //$NON-NLS-2$
    props.setProperty( "useMap", imageMapName != null ? "usemap=\"#" + imageMapName + "\"" : "" ); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
    props.setProperty( "contextPath", contextPath ); //$NON-NLS-1$
    props.setProperty( "chartWidth", Integer.toString( chartWidth ) ); //$NON-NLS-1$
    props.setProperty( "chartHeight", Integer.toString( chartHeight ) ); //$NON-NLS-1$
    props.setProperty( "imageFile", imageFile.getName() ); //$NON-NLS-1$

    return TemplateUtil.applyTemplate( JFREE_CHART_HTML_TEMPLATE, props, null );
  }

  public String createChartAsHtml( IPentahoSession userSession, Map<String, Object> parameterMap,
      String serializedChartDataDefinition, String serializedChartModel, int chartWidth, int chartHeight,
      String contentLinkingTemplate ) throws IOException {

    ChartModel chartModel = ChartSerializer.deSerialize( serializedChartModel, ChartSerializationFormat.JSON );

    String html = null;

    if ( chartModel.getChartEngineId() == null ) {
      // Load default value from system setting or take hard coded
      // Hard coded final fall back is Open Flash Chart
      String defaultChartEngine =
          PentahoSystem.getSystemSetting( "chartbeans/chartbeans_config.xml", "default-chart-engine",
              OpenFlashChartPlugin.PLUGIN_ID ); //$NON-NLS-1$ //$NON-NLS-2$
      if ( defaultChartEngine == null ) {
        defaultChartEngine = OpenFlashChartPlugin.PLUGIN_ID;
      }
      chartModel.setChartEngineId( defaultChartEngine );
    }
    // Check for render engine override
    String override = (String) parameterMap.get( "renderEngine" );
    if ( override != null ) {
      chartModel.setChartEngineId( override );
    }

    serializedChartModel = ChartSerializer.serialize( chartModel, ChartSerializationFormat.JSON );

    if ( contentLinkingTemplate == null ) { // use old version
      html =
          createChartAsHtml( userSession, parameterMap, serializedChartDataDefinition, serializedChartModel,
              chartWidth, chartHeight );
    } else { // with content linking
      final String ENCODING = "UTF-8"; //$NON-NLS-1$

      InputStream is =
          this.internalCreateChart( userSession, parameterMap, serializedChartDataDefinition, serializedChartModel,
              chartWidth, chartHeight, contentLinkingTemplate, null );
      html = IOUtils.toString( is, ENCODING ); // copy(is, writer);
    }

    return html;
  }

  public String createChartAsHtml( IPentahoSession userSession, Map<String, Object> parameterMap,
      String serializedChartDataDefinition, String serializedChartModel, int chartWidth, int chartHeight )
    throws IOException {

    ChartModel chartModel = ChartSerializer.deSerialize( serializedChartModel, ChartSerializationFormat.JSON );

    String html = null;

    if ( chartModel.getChartEngineId() == null ) {
      // Load default value from system setting or take hard coded

      // Hard coded final fall back is Open Flash Chart
      String defaultChartEngine =
          PentahoSystem.getSystemSetting( "chartbeans/chartbeans_config.xml", "default-chart-engine",
              OpenFlashChartPlugin.PLUGIN_ID ); //$NON-NLS-1$ //$NON-NLS-2$

      if ( defaultChartEngine == null ) {
        defaultChartEngine = OpenFlashChartPlugin.PLUGIN_ID;
      }

      chartModel.setChartEngineId( defaultChartEngine );

    }

    // Check for render engine override
    String override = (String) parameterMap.get( "renderEngine" );
    if ( override != null ) {
      chartModel.setChartEngineId( override );
    }

    serializedChartModel = ChartSerializer.serialize( chartModel, ChartSerializationFormat.JSON );

    if ( JFreeChartPlugin.PLUGIN_ID.equals( chartModel.getChartEngineId() ) ) {
      final String SOLUTION_TMP_DIR = "system/tmp/"; //$NON-NLS-1$
      // create tmp dir if it doesn't exist yet
      File tmpDir = new File( PentahoSystem.getApplicationContext().getFileOutputPath( SOLUTION_TMP_DIR ) );
      tmpDir.mkdirs();
      File chartFileOnServer = new File( tmpDir, java.util.UUID.randomUUID().toString() );

      BufferedOutputStream bos = null;
      try {
        bos = new BufferedOutputStream( new FileOutputStream( chartFileOnServer ) );
        this.internalCreateChart( userSession, parameterMap, serializedChartDataDefinition, serializedChartModel,
            chartWidth, chartHeight, null, bos );
      } finally {
        IOUtils.closeQuietly( bos );
      }

      IPentahoRequestContext requestContext = PentahoRequestContextHolder.getRequestContext();
      String contextPath = requestContext.getContextPath();
      String url = contextPath + ( ( !StringUtils.isEmpty( contextPath )
          && contextPath.endsWith( URL_PATH_SEP ) ) ? "" : URL_PATH_SEP ); //$NON-NLS-1$
      final String IMAGE_URL_TEMPLATE = "{0}getImage?image={1}"; //$NON-NLS-1$
      final String imageUrl =
          MessageFormat.format( IMAGE_URL_TEMPLATE, new String[] { url, chartFileOnServer.getName() } );
      html = this.mergeStaticImageHtmlTemplate( imageUrl );

    } else if ( OpenFlashChartPlugin.PLUGIN_ID.equals( chartModel.getChartEngineId() ) ) {

      ByteArrayOutputStream tmpOut = new ByteArrayOutputStream();
      this.internalCreateChart( userSession, parameterMap, serializedChartDataDefinition, serializedChartModel,
          chartWidth, chartHeight, null, tmpOut );
      final String ENCODING = "UTF-8"; //$NON-NLS-1$
      ByteArrayInputStream in = new ByteArrayInputStream( tmpOut.toByteArray() );
      IOUtils.closeQuietly( tmpOut );
      html = IOUtils.toString( in, ENCODING );
      IOUtils.closeQuietly( in );

    } else {
      throw new IllegalArgumentException( "unrecognized chart engine" ); //$NON-NLS-1$
    }

    return html;
  }

  public String mergeOpenFlashChartHtmlTemplate( String openFlashChartJson, String swfUrl ) {
    return buildOpenFlashChartHtmlFragment( openFlashChartJson, swfUrl, "100%", "100%" ); //$NON-NLS-1$ //$NON-NLS-2$
  }

  public String mergeJFreeChartHtmlTemplate( File imageFile, String imageMap, String imageMapName, int chartWidth,
      int chartHeight, String contextPath ) {
    return buildJFreeChartHtmlFragment( imageFile, imageMap, imageMapName, chartWidth, chartHeight, contextPath );
  }

  public String mergeStaticImageHtmlTemplate( String imageUrl ) {
    final String BODY_TEMPLATE = "<img src=\"{0}\" />"; //$NON-NLS-1$
    final String body = MessageFormat.format( BODY_TEMPLATE, new String[] { imageUrl } );
    return MessageFormat.format( getHtmlTemplate(), new String[] { "", body } ); //$NON-NLS-1$
  }

  public String getHtmlTemplate() {
    return DEFAULT_HTML_TEMPLATE;
  }

  public String getFlashScriptFragment() {
    return DEFAULT_flashScriptFragment;
  }

  public String getFlashObjectFragment() {
    return DEFAULT_flashObjectFragment;
  }

}
